<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MP3 模拟麦克风输入 Demo</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
        }
        .container {
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
            border: 1px solid #eee;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 30px;
        }
        .control-group {
            margin-bottom: 25px;
        }
        label {
            display: block;
            margin-bottom: 8px;
            font-weight: 600;
            color: #555;
        }
        input[type="file"] {
            padding: 8px;
            width: 100%;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .btn-group {
            display: flex;
            gap: 15px;
            margin-top: 10px;
        }
        button {
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s;
        }
        #startSimulate {
            background-color: #28a745;
            color: white;
        }
        #startSimulate:disabled {
            background-color: #94d3a2;
            cursor: not-allowed;
        }
        #stopSimulate {
            background-color: #dc3545;
            color: white;
        }
        #stopSimulate:disabled {
            background-color: #e8a7ad;
            cursor: not-allowed;
        }
        .preview-area {
            margin-top: 30px;
            padding-top: 20px;
            border-top: 1px dashed #ddd;
        }
        .preview-title {
            margin-bottom: 15px;
            color: #555;
        }
        audio {
            width: 100%;
            margin-top: 10px;
        }
        .status {
            margin-top: 15px;
            padding: 10px;
            border-radius: 4px;
            background-color: #f8f9fa;
            color: #666;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>MP3 模拟麦克风输入 Demo</h1>

        <!-- 1. MP3 文件上传区域 -->
        <div class="control-group">
            <label for="mp3File">选择本地 MP3 文件：</label>
            <input type="file" id="mp3File" accept="audio/mp3" />
            <div class="btn-group">
                <button id="startSimulate" disabled>开始模拟麦克风输入</button>
                <button id="stopSimulate" disabled>停止模拟</button>
            </div>
        </div>

        <!-- 2. 状态与预览区域 -->
        <div class="preview-area">
            <h3 class="preview-title">模拟流预览（录制结果）：</h3>
            <div id="audioPreview"></div> <!-- 动态插入音频播放器 -->
            <div class="status" id="status">请选择 MP3 文件并点击“开始模拟”</div>
        </div>
    </div>

    <script>
        // 核心变量初始化
        let audioContext;       // Web Audio 上下文（处理音频解码与播放）
        let bufferSource;       // 音频源节点（加载 MP3 缓冲区）
        let mediaStreamDest;    // 模拟 MediaStream 输出节点
        let simulatedStream;    // 最终模拟的“麦克风流”
        let mediaRecorder;      // 用于录制模拟流（预览用）
        let recordingChunks = [];// 录制的音频片段

        // 1. 初始化 AudioContext（需用户交互触发，浏览器安全限制）
        document.addEventListener('click', initAudioContext, { once: true });

        /**
         * 初始化 Web Audio 上下文
         */
        function initAudioContext() {
            // 兼容不同浏览器前缀（Chrome/Safari/Edge）
            window.AudioContext = window.AudioContext || window.webkitAudioContext;
            try {
                audioContext = new AudioContext();
                // 创建“模拟流输出节点”——核心是将音频流转为 MediaStream 格式
                mediaStreamDest = audioContext.createMediaStreamDestination();
                updateStatus('Audio 引擎初始化完成，请选择 MP3 文件', 'info');
            } catch (err) {
                updateStatus(`初始化失败：${err.message}（请使用最新版 Chrome/Firefox）`, 'error');
            }
        }

        // 2. 监听 MP3 文件上传，加载并解码
        document.getElementById('mp3File').addEventListener('change', async (e) => {
            const file = e.target.files[0];
            if (!file) return;
            if (!audioContext) {
                updateStatus('Audio 引擎未初始化，请先点击页面任意位置', 'error');
                return;
            }

            try {
                updateStatus(`正在加载 MP3 文件：${file.name}`, 'info');
                // 步骤1：将 MP3 文件转为 ArrayBuffer（二进制数据）
                const arrayBuffer = await readFileAsArrayBuffer(file);
                // 步骤2：用 AudioContext 解码 MP3 为 AudioBuffer（可播放的音频数据）
                const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
                
                // 解码成功，启用“开始模拟”按钮
                updateStatus(`MP3 解码完成：时长 ${audioBuffer.duration.toFixed(1)}s，声道数 ${audioBuffer.numberOfChannels}`, 'success');
                document.getElementById('startSimulate').disabled = false;
            } catch (err) {
                updateStatus(`文件处理失败：${err.message}（请确认是合法 MP3 文件）`, 'error');
                document.getElementById('startSimulate').disabled = true;
            }
        });

        // 3. 绑定“开始模拟”按钮事件
        document.getElementById('startSimulate').addEventListener('click', async () => {
            const file = document.getElementById('mp3File').files[0];
            if (!file) {
                updateStatus('请先选择 MP3 文件', 'error');
                return;
            }

            // 若 AudioContext 处于暂停状态（如切换标签后），恢复它
            if (audioContext.state === 'suspended') {
                await audioContext.resume();
            }

            try {
                // 重新加载并解码 MP3（确保每次播放都是完整音频）
                const arrayBuffer = await readFileAsArrayBuffer(file);
                const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);

                // 步骤1：创建音频源节点，加载解码后的音频
                bufferSource = audioContext.createBufferSource();
                bufferSource.buffer = audioBuffer;
                bufferSource.loop = true; // 循环播放（可根据需求改为 false）

                // 步骤2：连接节点——音频源 → 模拟流输出节点
                bufferSource.connect(mediaStreamDest);
                // 步骤3：获取模拟的“麦克风流”（关键：此流格式与硬件麦克风一致）
                simulatedStream = mediaStreamDest.stream;

                // 步骤4：启动音频播放与模拟流录制（预览用）
                bufferSource.start();
                startRecordingSimulatedStream();

                // 更新 UI 状态
                document.getElementById('startSimulate').disabled = true;
                document.getElementById('stopSimulate').disabled = false;
                updateStatus(`模拟输入已启动（正在循环播放 ${file.name}）`, 'success');
            } catch (err) {
                updateStatus(`模拟启动失败：${err.message}`, 'error');
                stopSimulate(); // 异常时清理资源
            }
        });

        // 4. 绑定“停止模拟”按钮事件
        document.getElementById('stopSimulate').addEventListener('click', stopSimulate);

        /**
         * 停止模拟输入，清理资源
         */
        function stopSimulate() {
            // 停止音频播放
            if (bufferSource) {
                bufferSource.stop();
                bufferSource.disconnect(); // 断开节点连接（释放资源）
                bufferSource = null;
            }

            // 停止模拟流录制
            if (mediaRecorder && mediaRecorder.state !== 'inactive') {
                mediaRecorder.stop();
            }

            // 关闭模拟流的所有轨道（防止内存泄漏）
            if (simulatedStream) {
                simulatedStream.getTracks().forEach(track => track.stop());
                simulatedStream = null;
            }

            // 更新 UI 状态
            document.getElementById('startSimulate').disabled = false;
            document.getElementById('stopSimulate').disabled = true;
            updateStatus('模拟输入已停止', 'info');
        }

        /**
         * 录制模拟流（用于预览）
         */
        function startRecordingSimulatedStream() {
            // 清空之前的录制数据
            recordingChunks = [];
            // 创建 MediaRecorder 实例（录制模拟流为 WAV 格式）
            mediaRecorder = new MediaRecorder(simulatedStream, { mimeType: 'audio/wav' });

            // 监听录制的音频片段
            mediaRecorder.ondataavailable = (e) => {
                if (e.data.size > 0) {
                    recordingChunks.push(e.data);
                }
            };

            // 录制结束：生成音频预览
            mediaRecorder.onstop = () => {
                // 创建音频 Blob 对象
                const audioBlob = new Blob(recordingChunks, { type: 'audio/wav' });
                // 生成可预览的 URL
                const audioUrl = URL.createObjectURL(audioBlob);
                
                // 插入音频播放器到页面
                const audioPreview = document.getElementById('audioPreview');
                audioPreview.innerHTML = `<audio src="${audioUrl}" controls>您的浏览器不支持音频预览</audio>`;
                updateStatus('模拟流录制完成，可预览音频', 'success');
            };

            // 开始录制
            mediaRecorder.start();
            updateStatus('正在录制模拟流（3秒后自动停止录制，用于预览）', 'info');
            // 3秒后自动停止录制（可根据需求调整时长）
            setTimeout(() => {
                if (mediaRecorder && mediaRecorder.state !== 'inactive') {
                    mediaRecorder.stop();
                }
            }, 3000);
        }

        /**
         * 工具函数：将 File 对象转为 ArrayBuffer
         */
        function readFileAsArrayBuffer(file) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = (e) => resolve(e.target.result);
                reader.onerror = (err) => reject(new Error(`文件读取失败：${err.message}`));
                reader.readAsArrayBuffer(file);
            });
        }

        /**
         * 工具函数：更新状态提示
         */
        function updateStatus(text, type = 'info') {
            const statusEl = document.getElementById('status');
            statusEl.textContent = text;
            // 切换状态样式
            statusEl.className = 'status';
            if (type === 'error') statusEl.style.color = '#dc3545';
            if (type === 'success') statusEl.style.color = '#28a745';
            if (type === 'info') statusEl.style.color = '#666';
        }

        // 页面关闭前清理资源
        window.addEventListener('beforeunload', () => {
            if (audioContext) audioContext.close();
            if (simulatedStream) simulatedStream.getTracks().forEach(track => track.stop());
        });
    </script>
</body>
</html>